#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#       Lynxy Wiki Engine [http://code.google.com/p/lynxywiki/].
#       Copyright 2009-2010 Dmitrij "Divius" Tantsur <divius.inside@gmail.com>
#
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.

"""Lynxy Wiki Engine.

This is the main script of Lynxy Wiki engine.
Just place it in server's cgi-bin directory and set some values below.

See README file for details.

"""

import cgi

### I. Debugging enabled (set LYNXY_DEBUG to False to disable)

LYNXY_DEBUG = True
if LYNXY_DEBUG:
    import cgitb
    cgitb.enable()


### II. Lynxy Basic Configuration

LYNXY_CONFIG = {
    # Real local paths
    "path" : {
        # Database file (not directory!), should be writtable,
        #  will be created if absent, SHOULD NOT be accessible from web
        "database"      : "./lynxy_data.db",
        # Lock file. SHOULD NOT EXIST!
        "lockfile"      : "./lynxy_data.db.lock",
        # Plugins code directory, should be read-only
        "plugins"       : "./lynxy_static/plugins",
        # Locales directory, should be read-only
        "locales"       : "./lynxy_static/locales",
        # Public data directory, is not used by Lynxy Core,
        #  should be writtable and accessible from web
        "data"          : "./lynxy_public",
    },
    # URL settings
    "url" : {
        # Base url, should contain Lynxy script if rewrite is not used
        "base"          : "/cgi-bin/lynxy.py"
    },
    # Language settings
    "lang" : {
        # Default language
        "__"            : "en",
        # List of supported languages and their names
        "supported" : {
            "en"        : "English",
            "ru"        : "Russian",
        }
    },
    # View settings
    "view" : {
        # Template file to use. Leave empty to use built-in template
        "template_file" : "",
    },
    "misc" : {
    },
    #Do not change
    "system" : {
        # Leave empty if you do not want anybody to see version
        "version"       : "0.0.1"
    },
}

### III. Lynxy Advanced Configuration

# Default template for Lynxy
LYNXY_DEFAULT_TEMPLATE = """
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>{{{%WIKI_TITLE%}}}{{ -  {%PAGE_TITLE%}}}</title>
    <style type="text/css">
        *{margin:0;padding:0;}
        body{font-size:12px; line-height:16px; padding:10px 20px 20px 20px;}
        p{margin:5px 0 5px 0;}
        pre{border:1px dotted #ccc; padding:4px; width:640px; overflow:auto; margin:3px;}
        img,a img{border:0px}
        h1,h2,h3,h4,h5,h6{letter-spacing:2px; font-weight:normal; margin:15px 0 15px 0px; color:#581F1F;}
        h1{margin:18px 0 15px 15px; font-size:22px;}
        hr{margin:10px 0 10px 0; height:0px; overflow:hidden; border:0px; border-top:1px solid #5F0697;}
        ul,ol{padding:5px 0px 5px 20px;}
        table{text-align:left; border-bottom:none;}
        a{text-decoration:none; color:#060684;}
        .error{color:#E21414; font-weight:bold;}
        .status{color:green; font-weight:bold;}
        .navbar a{color:black; border-bottom:1px dotted #4D4D4D;}
        #wikiTitleLink{font-weight:bold}
        #pageTitleLink{color:#581F1F;}
        .content a{border-bottom:1px dotted #4D4D4D;}
        form{display:inline}
        #renameForm{display:block; margin-bottom:6px;}
        #contentSubmit{margin-top:6px;}
        #contentTextarea{width:100%;}
        input,select,textarea{border:1px solid #AAAAAA; padding:2px; font-size:12px;}
        .submit{padding:1px;}
        textarea{padding:3px;}
        #poweredBy{font-style:italic}
        #lastChanged{font-style:italic}
        #toc{border:1px dashed #11141A; margin:5px 0 5px 10px; padding:6px 5px 7px 0px; float:right; padding-right:2em; list-style:none;}
        #toc ul{list-style:none; padding:3px 0 3px 10px;}
        #toc li{font-size:11px; padding-left:10px;}
        #toc ul li{font-size:10px;}
        #toc ul ul li{font-size:9px;}
        #toc ul ul ul li{font-size:8px;}
        #toc ul ul ul ul li{font-size:7px;}
        #diff ins{color:green; text-decoration:none; font-weight:bold;}
        #diff del{color:red; text-decoration:line-through;}
        #diff .orig{color:#666; font-size:90%;}
        {{{%STYLE_ADDONS%}}}
    </style>
    {{{%HEAD_ADDONS%}}}
    {{<META http-equiv="refresh" content="5; URL={%REDIRECT_TO%}">}}
</head>
<body>
<table border="0" width="100%" cellpadding="4" id="mainTable" cellspacing="1">
    {{<tr><td colspan="3">{%TOP_BANNER%}</td></tr>}}
    <tr>
        <th colspan="2">
            <h1><a href="{{{%CONFIG:url.base%}}}" id="wikiTitleLink">{{{%WIKI_TITLE%}}}</a>:
            <a href="{{{%CONFIG:url.base%}}}{{{%CURRENT_PAGE_URL%}}}" id="pageTitleLink">{{{%PAGE_TITLE%}}}</a></h1>
        </th>
    </tr>
    <tr>
        <td>
            <div class="navbar">
                {{{%NAVBAR%}}} {{{%NAVBAR_LEFT_ADDONS%}}}
            </div>
        </td>
        <td style="text-align:right;">
            <div class="navbar">
                <a href="{{{%CONFIG:url.base%}}}{{{%CURRENT_PAGE_URL%}}}?action=edit" class="editLink">{{{%TRANSLATE:Edit%}}}</a>
                <a href="{{{%CONFIG:url.base%}}}{{{%CURRENT_PAGE_URL%}}}?action=history" class="historyLink">{{{%TRANSLATE:History%}}}</a>
                {{{%NAVBAR_RIGHT_ADDONS%}}}
                <span class="search">{{{%SEARCH_FORM%}}}</span>
            </div>
        </td>
    </tr>
    {{<tr>
        <th colspan="2">
            <hr />
        </th>
    </tr>
    <tr>
        <td colspan="2">
            <div class="error">{%ERROR%}</div>
            <div>{%TRANSLATE:Try to return%} <a href="javascript:history.back()">{%TRANSLATE:back%}</a>
            {%TRANSLATE:or go to the main page%}: <a href="{%CONFIG:url.base%}">{%WIKI_TITLE%}</a>.</div>
        </td>
    </tr>}}
    {{<tr>
        <th colspan="2">
            <hr />
        </th>
    </tr>
    <tr>
        <td colspan="2">
            <div class="status">{%STATUS%}</div>
            <div>{%TRANSLATE:If you are not redirected automatically within a few seconds then please click on the link above.%}</div>
            <ul>
            {%@STATUS_LINKS>>%}
            <li><a href="{%@CONFIG:url.base%}{%@LINK_URL%}">{%LINK_TEXT%}</a></li>
            {%>>STATUS_LINKS%}
            <li><a href="{%@CONFIG:url.base%}">{%TRANSLATE:To the main page%}</a></li>
            </ul>
        </td>
    </tr>}}
    {{<tr>
        <th colspan="2">
            <hr />
        </th>
    </tr>
    <tr>
        <td colspan="2">
            <div class="content">
                <h2>{%TRANSLATE:History for page%} /{%@CURRENT_PAGE_URL%}</h2>
                {%REVISIONS>>%}
                <div class="historyItem">
                    <a href="{%CONFIG:url.base%}{%@CURRENT_PAGE_URL%}?rev={%REVNO%}">
                        <span class="historyRevno">{%TRANSLATE:Revision #%}{%REVNO%}</span>
                        <span class="historyLastChanged">[{%LAST_CHANGED%}]</span>
                        <span class="historyDescription">{%@DESCRIPTION%}</span>
                    </a>
                </div>
                {%>>REVISIONS%}
            </div>
        </td>
    </tr>}}
    {{<tr>
        <th colspan="2">
            <hr />
        </th>
    </tr>
    <tr>
        <td colspan="2">
            <div class="content">{%NOHTML:CONTENT%}</div>
        </td>
    </tr>}}
    {{<tr>
        <td colspan="2">
            <hr />
        </td>
    </tr>
    <tr>
        <td colspan="2">
            <div class="editForm">
                <form action="{%@CONFIG:url.base%}{%@CURRENT_PAGE_URL%}" method="post">
                    <input type="hidden" name="action" value="submit" />
                    <div>{%TRANSLATE:Title%}:</div>
                    <div><input style="width:100%" type="text" id="page_title" name="page_title" value="{%PAGE_TITLE%}" /></div>
                    <div>{%TRANSLATE:Contents%}:</div>
                    <div><textarea style="width:100%" rows="20" cols="50" id="content" name="content">{%EDIT_CONTENT%}</textarea></div>
                    <div>{%TRANSLATE:Changes description%}:</div>
                    <div><input style="width:100%" type="text" id="description" name="description" value="" /></div>
                    <div><input type="submit" value="{%TRANSLATE:Save%}" /> <input type="reset" value="{%TRANSLATE:Reset%}" /></div>
                </form>
            </div>
        </td>
    </tr>}}
    <tr>
        <td colspan="2">
            <hr />
        </td>
    </tr>
    <tr>
        <td>
            <div id="lastChanged">{{{%TRANSLATE:Last changed%}: {%LAST_CHANGED%}}}</div>
        </td>
        <td style="text-align:right;">
            <div class="navbar">
                <a href="{{{%CONFIG:url.base%}}}{{{%CURRENT_PAGE_URL%}}}?action=edit" class="editLink">{{{%TRANSLATE:Edit%}}}</a>
                <a href="{{{%CONFIG:url.base%}}}{{{%CURRENT_PAGE_URL%}}}?action=history" class="historyLink">{{{%TRANSLATE:History%}}}</a>
                {{{%BOTTOMBAR_RIGHT_ADDONS%}}}
                <span class="search">{{{%SEARCH_FORM%}}}</span>
            </div>
        </td>
    </tr>
    <tr>
        <td colspan="2">
            <hr />
        </td>
    </tr>
    <tr>
        <td>
            <div id="poweredBy" class="navbar">
                {{{%TRANSLATE:Powered by%}}} <a href="http://code.google.com/p/lynxy/">Lynxy Web Engine</a> {{{%CONFIG:system.version%}}}
            </div>
        </td>
        <td style="text-align:right;">
            <a href="http://validator.w3.org/check?uri=referer">
                <img src="http://www.w3.org/Icons/valid-xhtml10-blue"
                alt="Valid XHTML 1.0 Strict" height="31" width="88" /></a>
            <a href="http://jigsaw.w3.org/css-validator/check/referer">
                <img style="border:0;width:88px;height:31px"
                src="http://jigsaw.w3.org/css-validator/images/vcss-blue"
                alt="Valid CSS!" /></a>
        </td>
    </tr>
    {{<tr><td colspan="2">{%BOTTOM_BANNER%}</td></tr>}}
</table>
</body>
</html>
"""

### IV. Public API

### IV.I Imports and constants (except _)

LYNXY_VERSION = 10

import os
import shelve
import zlib
import time
import re
try:
    import ConfigParser as configparser
except ImportError:
    # Renamed in Python 3.0
    import configparser
try:
    from hashlib import sha1 as hash_function
except ImportError:
    # No hashlib module in python 2.4
    from sha import new as hash_function

# Add our environment to configuration to be able to access it via LynxyConfig
LYNXY_CONFIG["env"] = dict(os.environ)

### IV.II Exceptions

class LynxyDatabaseException(Exception):
    """Exception that is rised when database is corrupted or unaccessible."""

class LynxyPageNotFoundException(Exception):
    """Exception that is rised when requested page is not in database."""

class LynxyRevisionNotFoundException(Exception):
    """Exception that is rised when requested revision is not in database."""

class LynxyTranslationFileException(Exception):
    """Exception that is rised when translation file is malformed."""
    pass

### IV.III Auxiliary classes

class LynxyConfig(object):
    """Class representing configuration.

    Configuration is read from a dictionary.
    Consider examples:
        test = {"a":"b", "c":{"__":"def", "d":"e"}, "f":{"g":"h", "i":"j"}}
        conf = LynxyConfig(test)
        conf.a          --> LynxyConfig(test["a"])
        conf.a()        --> "b"
        conf.c          --> LynxyConfig(test["c"])
        conf.c()        --> conf.c.__()         --> "def"
        conf.c.d        --> LynxyConfig(test["c"]["d"])
        conf.c.d()      --> "e"
        conf.f          --> LynxyConfig(test["f"])
        conf.f()        --> ["g", "i"]
        conf.z          --> LynxyConfig(None)
        conf.z()        --> ""
        conf.z("value") --> "value"
        and so on

    That is, a subdictionary is represented as it's "__" value if present,
    otherwise as a list of it's keys.

    """

    __slots__ = ['config']

    def __init__(self):
        """Construct object to access configuration."""
        self.config = LYNXY_CONFIG

    def set(self, configuration):
        """Set configuration to work on and return a copy."""
        try:
            self.config = dict(configuration)
        except (ValueError, TypeError):
            self.config = configuration
        return self

    def valid(self):
        """Check whether configuration is not None."""
        return self.config is not None

    def value(self, path):
        """Return value by it's relative path."""
        path_list = [x for x in path.split(".") if x]
        conf = self
        for item in path_list:
            conf = conf.__getattr__(item)
        return conf()

    def __getattr__(self, name):
        """Get configuration subentry."""
        if type(self.config) is dict:
            try:
                attr = self.config[name]
            except KeyError:
                attr = None
        else:
            attr = None

        return LynxyConfig().set(attr)

    def __call__(self, default=""):
        """Get configuration value."""
        if type(self.config) is dict:
            if "__" in self.config:
                return self.config["__"]
            else:
                return self.config.keys()

        if self.config is not None:
            return self.config
        else:
            return default

    def __contains__(self, key):
        """Check whether key is in configuration."""
        if type(self.config) is dict:
            return key in self.config
        else:
            return False

    def __len__(self):
        """Get length of dictionary.

        If current configuration is not a dictionary returns 0.

        """
        if type(self.config) is dict:
            return len(self.config)
        else:
            return 0

class LynxyTranslator(object):
    """A class for managing translations.

    Use:
        _ = LynxyTranslator()
        print _("Some text")
        print _("Text from translation domain", domain="domain")

    Translation files should be in INI format, default section is "default",
        all "equal" signs in source text should be escaped as "&eq;",
        all square brackets should be escaped as "&lbr;" and "&rbr;",
        all "&" signs should be escaped as "&amp;".

    """

    def __init__(self, additional_sources=None):
        """Initialize translator."""
        self.translation = configparser.RawConfigParser()
        sources_list = [os.path.join(LynxyConfig().path.locales(),
                "lynxy_" + LynxyConfig().lang() + ".locale")]
        if additional_sources:
            sources_list.extend(additional_sources)
        # We don't want the whole procedure to fail because of one file
        for source in sources_list:
            try:
                self.translation.read([source])
            except configparser.ParsingError:
                raise LynxyTranslationFileException("Parse error while "
                    "parsing translation file %s" % source)

    def __call__(self, text, domain="default"):
        """Get translation for text from domain."""
        escaped = text.replace("&", "&amp;").replace("=", "&eq;").replace(
            "[", "&lbr;").replace("]", "&rbr;")

        try:
            text = self.translation.get(domain, escaped)
        except (configparser.NoSectionError, configparser.NoOptionError):
            return text

        return text.replace("&eq;", "=").replace("&lbr;", "[").replace(
            "&rbr;", "]").replace("&amp;", "&")

class LynxyDatabase(object):
    """Database handling class.

    This is high-level interface to low-level database storage.
    Use open() or lock() to open a database and close() to close it.

    """

    __slots__ = ['__db']

    def __init__(self):
        """Initialize database object."""
        self.__db = None

    def open(self, lock=False, timeout=10.0):
        """Open database with optional locking.

        Returns reference to self.

        """
        # Close and unlock before opening
        self.close()

        # Simply try opening a database file to ensure it exists
        shelve.open(LynxyConfig().path.database()).close()

        # Acquire lock using atomic nature of link(2)
        if lock:
            endtime = time.time() + timeout
            wait = timeout / 20
            while True:
                try:
                    os.link(LynxyConfig().path.database(),
                        LynxyConfig().path.lockfile())
                    break
                except os.error:
                    if time.time() > endtime:
                        raise LynxyDatabaseException(
                            _("Database is permanently locked, "
                            "contact webmaster"))
                    time.sleep(wait)

        # Ok, we have lock, let us open database
        try:
            self.__db = shelve.open(LynxyConfig().path.database())
        except:
            raise LynxyDatabaseException(_("Cannot open database file"))

        # Assure we have correct mode
        try:
            os.chmod(LynxyConfig().path.database(), 0600)
        except os.error:
            pass

        return self

    def lock(self, timeout=10.0):
        """A synonim for open(lock=True, timeout=timeout)."""
        return self.open(lock=True, timeout=timeout)

    def opened(self):
        """Check whether database is opened."""
        return self.__db is not None

    def close(self):
        """Close the database.

        Does nothing if database wasn't opened.

        """
        # None means database wasn't opened
        if self.__db is None:
            return

        # Close shelve object
        self.__db.close()

        # Unlock if required
        if os.path.exists(LynxyConfig().path.lockfile()):
            try:
                os.unlink(LynxyConfig().path.lockfile())
            except os.error:
                raise LynxyDatabaseException(
                    _("Database cannot be unlocked, "
                    "contact webmaster"))

        # Mark database as closed
        self.__db = None

    def repair(self):
        """Repair database.

        Database must NOT be opened.

        """
        # Yeap, it really must not=)
        if self.__db is not None:
            raise LynxyDatabaseException(
                _("Repair called for opened database."))

        # Now open
        self.lock()
        try:
            if not "$version" in self.__db:
                self.__db["$version"] = LYNXY_VERSION

            if not "$WIKI_TITLE" in self.__db:
                self.__db["$WIKI_TITLE"] = "Lynxy"

            if not "/" in self.__db:
                timestamp = time.time()
                # Generate dummy main page with greeting
                self.__db["/"] = {
                    "current_revision"  : 1,
                    "cache"             : None,
                    "#1" : self.store_block(
                        "== Welcome to Lynxy ==\n"
                        "Edit this page to create your main page\n\n"
                        "Best regards,\nLynxy team\n"
                        "[[http://code.google.com/p/lynxy/]]",
                        {'$PAGE_TITLE'  : 'Main Page',
                        '$TIMESTAMP'    : timestamp,
                        '$LAST_CHANGED' : time.strftime(
                            "%d.%m.%Y %H:%M:%S +0000",
                            time.gmtime(timestamp)),
                        '$DESCRIPTION'  : 'Initial built-in version'})
                }
        finally:
            self.close()

    def store_block(self, text, metadata = None):
        """Store text block in database if not previously stored."""
        if metadata is None:
            metadata = {}
        text_digest = hash_function(text).hexdigest()
        newid = "@" + text_digest
        if newid not in self.__db:
            self.__db[newid] = zlib.compress(text)

        digest = hash_function("%s%f" % (text_digest, time.time())).hexdigest()
        self.__db["%m%" + digest] = metadata
        self.__db["%b%" + digest] = text_digest
        return digest

    def get_url_info(self, url):
        """Get a record for url.

        Returns None if record doesn't exist.

        """
        try:
            return self.__db[url]
        except KeyError:
            return None

    def store_url_info(self, url, url_info):
        """Store a record for url."""
        self.__db[url] = url_info

    def get_metadata(self, digest):
        """Get metadata by revision digest.

        Returns empty dict if no metadata.

        """
        try:
            return self.__db["%m%" + digest]
        except KeyError:
            return {}

    def get_block(self, digest):
        """Get text block by revision digest.

        Returns None if no such block present.

        """
        try:
            text_digest = self.__db["%b%" + digest]
            return zlib.decompress(self.__db["@" + text_digest])
        except KeyError:
            return None

    def __contains__(self, key):
        """Check whether database has this key.

        This is a low-level method, use high-level methods like
        get_block() if possible.

        """
        return key in self.__db

    def __getitem__(self, key):
        """Get key from database.

        This is a low-level method, use high-level methods like
        get_block() if possible.

        """
        return self.__db[key]

    def __iter__(self):
        """Get an iterator over all items.

        This is a low-level method, use high-level methods like
        get_block() if possible.

        """
        return iter(self.__db)

### IV.IV Main API classes

class LynxyParser(object):
    """Class representating markup parser.

    This class itself provides only simple fallback formatting
    and relies on plugins for something more complex.

    """

    def __init__(self):
        """Initialize parser."""
        pass

    def parse(self, text):
        """Parse markup in text."""
        # TODO: plugins support in LynxyParser.parse

        def __replace_expr(text, expr, replace):
            """Replace regular expression in text."""
            while True:
                match = expr.search(text)
                if match is None:
                    break

                index = 1
                template = replace
                for group in match.groups():
                    template = template.replace("(:%d)" % index, group)
                    index += 1

                text = text[:match.start()] + template + text[match.end():]
            return text

        text = cgi.escape(text, True)
        text = __replace_expr(text,
            re.compile("^\s*====(.+?)====\s*$", re.MULTILINE),
            "<h4>(:1)</h4>")
        text = __replace_expr(text,
            re.compile("^\s*===(.+?)===\s*$", re.MULTILINE),
            "<h3>(:1)</h3>")
        text = __replace_expr(text,
            re.compile("^\s*==(.+?)==\s*$", re.MULTILINE),
            "<h2>(:1)</h2>")
        text = __replace_expr(text,
            re.compile("^\s*=(.+?)=\s*$", re.MULTILINE),
            "<h1>(:1)</h1>")

        text = __replace_expr(text,
            re.compile("\[\[/(.*?)\|([^\|]+?)\]\]", re.MULTILINE),
            "<a href=\"" + LynxyConfig().url.base() + "/(:1)\">(:2)</a>")
        text = __replace_expr(text,
            re.compile("\[\[/(.*?)\]\]", re.MULTILINE),
            "<a href=\"" + LynxyConfig().url.base() + "/(:1)\">" +
            LynxyConfig().url.base() + "/(:1)</a>")

        text = __replace_expr(text,
            re.compile("\[\[(.+?)\|([^\|]+?)\]\]", re.MULTILINE),
            "<a href=\"(:1)\">(:2)</a>")
        text = __replace_expr(text,
            re.compile("\[\[(.+?)\]\]", re.MULTILINE),
            "<a href=\"(:1)\">(:1)</a>")

        text = __replace_expr(text, re.compile("(\n\r|\r\n|\r|\n)"), "<br />")

        return text

class LynxyDocument(object):
    """Class representating document."""

    def __init__(self, url, form):
        """Create a document for url."""
        # Remove duplicates and trailing slash
        self.url = "/" + "/".join(x for x in url.split("/") if x)

        self.form = form
        self.url_info = None
        self.revision = None
        self.metadata = None
        self.contents = None

    def load(self, parse_contents=True, skip_contents=False):
        """Load document from database.

        Set parse_contents to False if you do not want contents to be parsed,
        set skip_contents to True if you do not want it to be even loaded.

        Can raise LynxyPageNotFoundException and
        LynxyRevisionNotFoundException.

        """
        database = LynxyDatabase().open()
        try:
            self.url_info = database.get_url_info(self.url)
            if self.url_info is None:
                raise LynxyPageNotFoundException(_("Page %s not found",
                    domain="exceptions") % self.url)

            try:
                cached_contents = self.url_info["cache"]
            except KeyError:
                cached_contents = None

            try:
                head_revision = int(self.url_info["current_revision"])
            except (KeyError, ValueError):
                raise LynxyRevisionNotFoundException(
                    _("No head revision for page %s", domain="exceptions") %
                    self.url)

            try:
                revision = int(self.form.getfirst("rev") or 0)
            except ValueError:
                # Malformed revision spec, defaulting to head
                revision = head_revision

            # revision = 0 or None equals head_revision
            if not revision:
                revision = head_revision
            # revision < 0 equals head_revision - abs(revision)
            elif revision < 0:
                revision = head_revision + revision

            if revision < 1 or revision > head_revision:
                raise LynxyRevisionNotFoundException(
                    _("No revision %d for page %s", domain="exceptions"
                    ) % (revision, self.url))

            # Store current revision
            self.revision = revision

            # Get revision digest
            try:
                digest = self.url_info["#%d" % revision]
            except KeyError:
                raise LynxyRevisionNotFoundException(
                    _("Broken revision %d for page %s", domain="exceptions"
                    ) % (revision, self.url))

            # Load metadata
            self.metadata = database.get_metadata(digest)

            if not skip_contents:
                # If we are asked for head revision and cache is present
                # just return version from cache, otherwise load it
                if revision == head_revision and cached_contents is not None \
                    and parse_contents:
                    self.contents = cached_contents
                else:
                    self.contents = database.get_block(digest)
                    if parse_contents:
                        self.contents = LynxyParser().parse(self.contents)
        finally:
            database.close()

        # Store contents in cache if possible
        if revision == head_revision and self.contents is not None \
            and parse_contents:
            database = LynxyDatabase().lock()
            try:
                self.url_info["cache"] = self.contents
                database.store_url_info(self.url, self.url_info)
            finally:
                database.close()

        return self.contents

    def store(self, content):
        """Create a revision with given content and switch to it."""
        database = LynxyDatabase().lock()
        try:
            self.url_info = database.get_url_info(self.url)
            if self.url_info is None:
                raise LynxyPageNotFoundException(_("Page %s not found",
                    domain="exceptions") % self.url)

            try:
                head_revision = int(self.url_info["current_revision"])
            except (KeyError, ValueError):
                raise LynxyRevisionNotFoundException(
                    _("No head revision for page %s", domain="exceptions") %
                    self.url)

            # Get head revision
            try:
                head_digest = self.url_info["#%d" % head_revision]
            except KeyError:
                raise LynxyRevisionNotFoundException(
                    _("Broken revision %d for page %s", domain="exceptions"
                    ) % (head_revision, self.url))

            metadata = database.get_metadata(head_digest)

            # Create a revision
            timestamp = time.time()

            revision = head_revision + 1

            metadata['$PAGE_TITLE'] = self.form.getfirst("page_title") or \
                metadata['$PAGE_TITLE']
            metadata['$DESCRIPTION'] = self.form.getfirst("description") or ""
            metadata['$TIMESTAMP'] = timestamp
            metadata['$LAST_CHANGED'] = time.strftime(
                "%d.%m.%Y %H:%M:%S +0000", time.gmtime(timestamp))

            self.url_info["#%d" % revision] = database.store_block(
                content, metadata)
            self.url_info["cache"] = None
            self.url_info["current_revision"] = revision
            database.store_url_info(self.url, self.url_info)

            self.revision = 0
            self.metadata = metadata
        finally:
            database.close()

class LynxyTemplate(object):
    """Class representating template parser."""

    def __init__(self, template=None):
        """Initialize selected template.

        If template is None uses global configuration.

        """
        if template is None:
            template = LYNXY_DEFAULT_TEMPLATE
            filename = LynxyConfig().view.template_file()
            if filename:
                try:
                    template = open(filename).read()
                except (os.error, IOError):
                    pass

        self.template = template

    __reTemplate = re.compile("[^{]{{(.+?)}}[^}]", re.DOTALL)
    __reSubTemplate = re.compile("{%(.+?)%}", re.DOTALL)

    def __process_subtemplates(self, database, template, document, args):
        """Parse subtemplates in a template."""
        non_empty = False
        while True:
            match = self.__reSubTemplate.search(template)
            if match is None:
                break

            non_empty = True

            varname = match.group(1).strip()
            replacement = ""

            optional = False
            if varname.startswith("@"):
                optional = True
                varname = varname[1:]

            no_html = False
            if varname.startswith("NOHTML:"):
                no_html = True
                varname = varname[len("NOHTML:"):]

            loop = False
            if varname.endswith(">>"):
                loop = True
                varname = varname[:-2]

            if varname in args and args[varname]:
                replacement = args[varname]
            elif varname == "CURRENT_PAGE_URL":
                if document.url != "/":
                    replacement = document.url
            elif varname == "NAVBAR":
                replacement = document.url
            elif varname.startswith("TRANSLATE:"):
                replacement = _(varname[len("TRANSLATE:"):], domain="template")
            elif varname.startswith("CONFIG:"):
                replacement = LynxyConfig().value(varname[len("CONFIG:"):])
            elif document.metadata and "$" + varname in document.metadata:
                replacement = document.metadata["$" + varname]
            elif "$" + varname in database:
                replacement = database["$" + varname]

            if not replacement and not optional:
                return ""

            if loop:
                endloop = re.compile("{%%>>%s%%}" % varname).search(template,
                    match.end())
                if endloop is None:
                    template = template[:match.start()] + \
                        template[match.end():]
                    continue

                text = ""
                if replacement:
                    for loop_vars in replacement:
                        new_args = args
                        if type(loop_vars) is dict:
                            for key in loop_vars:
                                new_args[key] = loop_vars[key]

                        text += self.__process_subtemplates(database,
                            template[match.end():endloop.start()],
                            document, new_args)

                template = template[:match.start()] + text + \
                    template[endloop.end():]
            else:
                replacement = str(replacement)
                if not no_html:
                    replacement = cgi.escape(replacement, True)

                template = template[:match.start()] + replacement + \
                    template[match.end():]

        if non_empty:
            return template
        else:
            return ""

    def process(self, document, args):
        """Parse template variables for document."""
        # TODO: Plugins support in LynxyTemplate.process

        result = self.template
        database = LynxyDatabase().open()
        try:
            while True:
                match = self.__reTemplate.search(result)
                if match is None:
                    break

                replacement = self.__process_subtemplates(database,
                    match.group(1), document, args)

                result = result[:match.start() + 1] + replacement + \
                    result[match.end() - 1:]

            return result
        finally:
            database.close()

class LynxyInstance(object):
    """Main class for Lynxy.

    You can use this class from your scripts.
    Just create it and call process().

    """

    __slots__ = ["form", "action", "document"]

    def __init__(self, form):
        """Create an instance.

        form should be cgi.FieldStorage or compatible object.

        """
        # Assure we have correct database file
        LynxyDatabase().repair()

        self.form = form
        self.action = form.getfirst("action", "view")
        self.document = None

    @staticmethod
    def headers():
        """Headers that should be sent.

        Does not include trailing empty line!
        Note that although this method is static now, it can be made
        regular method some day.

        """
        return ("Content-Type: text/html",)

    def process(self):
        """Process request."""
        template_args = {}

        self.document = LynxyDocument(url=LynxyConfig().env.PATH_INFO(),
            form=self.form)

        try:
            if self.action == "view":
                # View contents at selected revision
                template_args["CONTENT"] = self.document.load()
            elif self.action == "edit":
                # Edit content aka add new revision based on head
                template_args["EDIT_CONTENT"] = \
                    self.document.load(parse_contents=False)
            elif self.action == "history":
                # View revision history
                history = []
                self.document.load(skip_contents=True)
                database = LynxyDatabase().open()
                try:
                    revisions = []
                    for key in self.document.url_info:
                        if not key.startswith("#"):
                            continue
                        try:
                            revno = int(key[1:])
                        except ValueError:
                            continue
                        revisions.append(revno)

                    revisions.sort(reverse=True)

                    for revno in revisions:
                        revdata = database.get_metadata(
                            self.document.url_info["#%d" % revno])

                        if "$DESCRIPTION" in revdata and \
                            revdata["$DESCRIPTION"]:
                            description = revdata["$DESCRIPTION"]
                        else:
                            description = _("No description")
                        history.append({"REVNO" : revno,
                            "LAST_CHANGED" : revdata["$LAST_CHANGED"],
                            "DESCRIPTION" : description})
                finally:
                    database.close()

                template_args["REVISIONS"] = history
            elif self.action == "submit":
                # Store content in database
                self.document.store(self.form.getfirst("content") or "")
                template_args["STATUS"] = _("Page saved successfully")
                template_args["REDIRECT_TO"] = LynxyConfig().url.base() + \
                    self.document.url
                template_args["STATUS_LINKS"] = [
                    {"LINK_URL" : self.document.url,
                    "LINK_TEXT" : _("Return to edited page")}
                ]
            else:
                raise LynxyPageNotFoundException(_("Unknown action '%s'",
                    domain="exceptions") % self.action)
        except (LynxyPageNotFoundException, LynxyRevisionNotFoundException), \
            exc:
            template_args["ERROR"] = str(exc)
        except LynxyDatabaseException, exc:
            template_args["ERROR"] = \
                _("Internal database corruption: %s") % str(exc)
        except Exception:
            if LYNXY_DEBUG:
                raise
            else:
                template_args["ERROR"] = \
                    _("Unknown error in lynxy.LynxyInstance.process()")

        self.response(self.headers(),
            LynxyTemplate().process(self.document, template_args))

    def response(self, headers, content):
        """Output a response."""
        for header in headers:
            print header
        print
        print content

    def redirect(self, location):
        """Redirect user to another page."""
        print "Location: %s" % (LynxyConfig().url.base() + location)
        print

### V. Main routine

# Create toplevel translator
_ = LynxyTranslator()

def main():
    """Default main function for Lynxy Wiki Engine."""
    instance = LynxyInstance(cgi.FieldStorage())
    #print "Content-Type: text/html"
    #print
    instance.process()

if __name__ == '__main__':
    main()
